package aceim.protocol.snuk182.vkontakte.internal; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.Executors; import aceim.api.dataentity.BuddyGroup; import aceim.api.dataentity.ConnectionState; import aceim.api.dataentity.ItemAction; import aceim.api.dataentity.Message; import aceim.api.dataentity.MessageAckState; import aceim.api.dataentity.MultiChatRoom; import aceim.api.dataentity.OnlineInfo; import aceim.api.dataentity.TextMessage; import aceim.api.dataentity.tkv.MessageAttachment; import aceim.api.dataentity.tkv.MessageAttachment.MessageAttachmentType; import aceim.api.service.ApiConstants; import aceim.api.service.ProtocolException; import aceim.api.utils.Logger; import aceim.api.utils.Logger.LoggerLevel; import aceim.protocol.snuk182.vkontakte.R; import aceim.protocol.snuk182.vkontakte.VkConstants; import aceim.protocol.snuk182.vkontakte.VkEntityAdapter; import aceim.protocol.snuk182.vkontakte.VkService; import aceim.protocol.snuk182.vkontakte.internal.VkEngine.LongPollCallback; import aceim.protocol.snuk182.vkontakte.internal.VkEngine.RequestFailedException; import aceim.protocol.snuk182.vkontakte.model.AccessToken; import aceim.protocol.snuk182.vkontakte.model.VkBuddy; import aceim.protocol.snuk182.vkontakte.model.VkBuddyGroup; import aceim.protocol.snuk182.vkontakte.model.VkChat; import aceim.protocol.snuk182.vkontakte.model.VkMessage; import aceim.protocol.snuk182.vkontakte.model.VkMessageAttachment; import aceim.protocol.snuk182.vkontakte.model.VkOnlineInfo; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Bundle; import android.text.TextUtils; public class VkServiceInternal { private ConnectionState connectionState = ConnectionState.DISCONNECTED; private final VkService service; private VkEngine engine; private AccessToken accessToken; private final IconDownloader iconDownloader = new IconDownloader(); private final Set<Long> connectedChats = new CopyOnWriteArraySet<Long>(); private final Set<VkChat> chats = new CopyOnWriteArraySet<VkChat>(); private final Map<Long, String> iconPaths = Collections.synchronizedMap(new HashMap<Long, String>()); private final LongPollCallback callback = new LongPollCallback() { @Override public void onlineInfo(VkOnlineInfo vi) { if (vi == null) { return; } OnlineInfo info; if (vi.getStatus() == 0) { info = requestStatusInternal(vi.getUid(), null); info.getFeatures().putByte(ApiConstants.FEATURE_STATUS, (byte) (vi.getStatus() == 0 ? 0 : -1)); } else { info = VkEntityAdapter.vkOnlineInfo2OnlineInfo(vi, accessToken.getUserID(), service.getProtocolUid(), service.getServiceId()); } service.getCoreService().buddyStateChanged(Arrays.asList(info)); } @Override public void message(VkMessage vkm) { //Despite of official documentation to say flag #16 is for "isChat", tests show it is not, but rather for "isAck". if (vkm.getPartnerId() != accessToken.getUserID() && vkm.isOutgoing() && !vkm.isChat()) { MessageAckState ackState; if (vkm.isUnread()){ ackState = MessageAckState.RECIPIENT_ACK; } else { ackState = MessageAckState.READ_ACK; } service.getCoreService().messageAck(Long.toString(vkm.getPartnerId()), vkm.getMessageId(), ackState); } else { Message message = VkEntityAdapter.vkMessage2Message(vkm, service.getServiceId(), service.getProtocolUid(), accessToken.getUserID()); if ((isChatUid(vkm.getPartnerId()) && !isChatJoined(vkm.getPartnerId())) // the case chat exists but not joined || message.getContactDetail() != null) { // the case chat has just been created joinChatInternal(message.getContactUid(), false); } if (message instanceof TextMessage) { processAttachments((TextMessage) message, vkm.getAttachments()); } service.getCoreService().message(message); } try { engine.markMessagesAsRead(new long[]{vkm.getMessageId()}); } catch (RequestFailedException e) { onRequestFailed(e); } } @Override public void typingNotification(long contactId, long chatParticipantId) { service.getCoreService().typingNotification(Long.toString(chatParticipantId != 0 ? chatParticipantId : contactId)); } @Override public void disconnected(String reason) { onLogout(reason); } }; private final Runnable proceedLoginRunnable = new Runnable() { @Override public void run() { if (accessToken != null) { SharedPreferences.Editor editor = service.getContext().getSharedPreferences(VkConstants.PREFS, Context.MODE_PRIVATE).edit(); editor.putString(VkConstants.KEY_TOKEN, accessToken.getToken()); editor.putLong(VkConstants.KEY_USER_ID, accessToken.getUserID()); editor.putLong(VkConstants.KEY_EXP_TIME_SECONDS, accessToken.getExpirationTime().getTime()); editor.putBoolean(VkConstants.KEY_UNEXPIRABLE_TOKEN, accessToken.isUnexpirable()); editor.commit(); proceedLogin(); } else { onLogout("Empty access token"); } } }; private final Runnable loginRunnable = new Runnable() { @Override public void run() { if (connectionState != ConnectionState.DISCONNECTED) { Logger.log("Already connected", LoggerLevel.INFO); return; } String token = service.getContext().getSharedPreferences(VkConstants.PREFS, Context.MODE_PRIVATE).getString(VkConstants.KEY_TOKEN, null); long userId = service.getContext().getSharedPreferences(VkConstants.PREFS, Context.MODE_PRIVATE).getLong(VkConstants.KEY_USER_ID, 0); long expirationTime = service.getContext().getSharedPreferences(VkConstants.PREFS, Context.MODE_PRIVATE).getLong(VkConstants.KEY_EXP_TIME_SECONDS, 0); boolean unexpirableToken = service.getContext().getSharedPreferences(VkConstants.PREFS, Context.MODE_PRIVATE).getBoolean(VkConstants.KEY_UNEXPIRABLE_TOKEN, false); if (token == null) { showLoginDialog(); } else { loginResult(token, expirationTime, unexpirableToken, userId); } } }; public VkServiceInternal(VkService service) { this.service = service; } private void processAttachments(TextMessage message, VkMessageAttachment[] vkAttachments) { if (vkAttachments == null || vkAttachments.length < 1) { return; } List<String> photos = new ArrayList<String>(vkAttachments.length); List<String> audios = new ArrayList<String>(vkAttachments.length); List<String> videos = new ArrayList<String>(vkAttachments.length); List<String> docs = new ArrayList<String>(vkAttachments.length); for (VkMessageAttachment attachment : vkAttachments) { switch (attachment.getType()) { case AUDIO: audios.add(attachment.getId()); break; case PHOTO: photos.add(attachment.getId()); break; case VIDEO: videos.add(attachment.getId()); break; case DOC: docs.add(attachment.getId()); break; default: break; } } List<MessageAttachment> attachments = message.getAttachments(); try { if (photos.size() > 0) { fillAttachments(engine.getPhotosById(photos.toArray(new String[photos.size()])), attachments, MessageAttachmentType.PHOTO); } if (videos.size() > 0) { //Vkontakte does not provide direct video links, but links to a page with video player. We cannot process it as VIDEO attachment type fillAttachments(engine.getVideosById(videos.toArray(new String[videos.size()])), attachments, MessageAttachmentType.OTHER); //MessageAttachmentType.VIDEO); } if (docs.size() > 0) { fillAttachments(engine.getDocsById(docs.toArray(new String[docs.size()])), attachments, MessageAttachmentType.OTHER); } if (audios.size() > 0) { fillAttachments(engine.getAudiosById(audios.toArray(new String[audios.size()])), attachments, MessageAttachmentType.AUDIO); } } catch (RequestFailedException e) { onRequestFailed(e); } } private void fillAttachments(Map<String, String> source, List<MessageAttachment> attachments, MessageAttachmentType type) { for (String src : source.keySet()) { String title = source.get(src); MessageAttachment attachment = new MessageAttachment(type, title, src); attachments.add(attachment); } } public void login(OnlineInfo info) { Executors.defaultThreadFactory().newThread(loginRunnable).start(); } private void proceedLogin() { if (!isTokenFresh()) { renewToken(); } else { this.engine = new VkEngine(accessToken.getToken(), Long.toString(accessToken.getUserID())); connect(); } } private void connect() { try { connectionState = ConnectionState.CONNECTING; service.getCoreService().connectionStateChanged(ConnectionState.CONNECTING, 1); List<VkBuddyGroup> groups = engine.getBuddyGroupList(); List<VkBuddy> buddies = engine.getBuddyList(); service.getCoreService().connectionStateChanged(ConnectionState.CONNECTING, 4); fillIconMap(buddies); List<VkOnlineInfo> onlineInfos = engine.getOnlineBuddies(); service.getCoreService().connectionStateChanged(ConnectionState.CONNECTING, 7); List<BuddyGroup> buddyList = VkEntityAdapter.vkBuddiesAndGroups2BuddyList(buddies, groups, onlineInfos, accessToken.getUserID(), service.getProtocolUid(), service.getServiceId()); connectionState = ConnectionState.CONNECTED; service.getCoreService().connectionStateChanged(ConnectionState.CONNECTED, 0); service.getCoreService().buddyListUpdated(buddyList); int pollWaitTime = Integer.parseInt(service.getCoreService().requestPreference(VkConstants.KEY_LONGPOLL_WAIT_TIME)); engine.connectLongPoll(pollWaitTime, callback); getAvailableGroupchats(); getMyPersonalInfoInternal(); getStatuses(onlineInfos); } catch (RequestFailedException e) { onRequestFailed(e); } } private void getStatuses(final List<VkOnlineInfo> onlineInfos) { if (engine == null) { onLogout(null); return; } Executors.defaultThreadFactory().newThread(new Runnable() { @Override public void run() { List<OnlineInfo> infos = new ArrayList<OnlineInfo>(onlineInfos.size()); for (VkOnlineInfo vkInfo : onlineInfos) { OnlineInfo info = requestStatusInternal(vkInfo.getUid(), onlineInfos); if (info != null) { infos.add(info); } } service.getCoreService().buddyStateChanged(infos); } }).start(); } private OnlineInfo requestStatusInternal(long uid, List<VkOnlineInfo> onlineInfos) { if (engine == null) { onLogout(null); return null; } String status; try { status = engine.requestStatus(uid); Logger.log(uid + " has status: " + status, LoggerLevel.VERBOSE); } catch (RequestFailedException e) { Logger.log(e); status = ""; } OnlineInfo info; if (accessToken.getUserID() == uid) { info = new OnlineInfo(service.getServiceId(), service.getProtocolUid()); info.setXstatusName(status); } else { info = new OnlineInfo(service.getServiceId(), Long.toString(uid)); info.setXstatusName(status); if (onlineInfos != null) { for (VkOnlineInfo vkInfo : onlineInfos) { if (vkInfo.getUid() == uid) { info.getFeatures().putByte(ApiConstants.FEATURE_STATUS, (byte) 0); break; } } } } return info; } public void leaveChat(final String chatId) { Executors.defaultThreadFactory().newThread(new Runnable() { @Override public void run() { connectedChats.remove(Long.parseLong(chatId)); service.getCoreService().buddyAction(ItemAction.LEFT, new MultiChatRoom(chatId, service.getProtocolUid(), VkConstants.PROTOCOL_NAME, service.getServiceId())); } }).start(); } public void joinChat(final String chatId, final boolean loadIcons) { Executors.defaultThreadFactory().newThread(new Runnable() { @Override public void run() { joinChatInternal(chatId, loadIcons); } }).start(); } private void joinChatInternal(final String chatId, boolean loadIcons) { try { if (engine == null) { onLogout(null); return; } if (!isChatUid(Long.parseLong(chatId))) { service.getCoreService().notification(service.getContext().getString(R.string.x_is_not_a_chat, chatId)); return; } final VkChat vkChat = engine.getChatById(chatId); List<VkBuddy> occupants = engine.getUsersByIdList(vkChat.getUsers()); MultiChatRoom chat = VkEntityAdapter.vkChat2MultiChatRoom(vkChat, service.getProtocolUid(), service.getServiceId()); chat.getOccupants().addAll(VkEntityAdapter.vkChatOccupants2ChatOccupants(vkChat, occupants, accessToken.getUserID(), service.getProtocolUid(), service.getServiceId())); chat.getOnlineInfo().getFeatures().putByte(ApiConstants.FEATURE_STATUS, (byte) 0); chats.add(vkChat); connectedChats.add(vkChat.getId()); service.getCoreService().buddyAction(ItemAction.JOINED, chat); service.getCoreService().buddyStateChanged(Arrays.asList(chat.getOnlineInfo())); /* * List<VkMessage> vkMessages = * engine.getLastChatMessages(chat.getProtocolUid(), true); * * for (VkMessage vkm : vkMessages) { * service.getCoreService().message * (VkEntityAdapter.vkChatMessage2Message(vkChat.getId(), vkm, * service.getServiceId())); } */ if (loadIcons) { Executors.defaultThreadFactory().newThread(new Runnable() { @Override public void run() { for (long uid : vkChat.getUsers()) { requestIcon(Long.toString(uid)); } } }); } } catch (RequestFailedException e) { onRequestFailed(e); } } public void typingNotification(final String uid) { Executors.defaultThreadFactory().newThread(new Runnable() { @Override public void run() { if (engine == null) { onLogout(null); return; } try { engine.sendTypingNotifications(uid, isChatUid(Long.parseLong(uid))); } catch (NumberFormatException e) { Logger.log(e); } catch (RequestFailedException e) { onRequestFailed(e); } } }).start(); } private void getMyPersonalInfoInternal() { if (engine == null) { onLogout(null); return; } try { VkBuddy myInfo = engine.getMyInfo(); OnlineInfo info = requestStatusInternal(accessToken.getUserID(), null); info.getFeatures().putBoolean(VkApiConstants.FEATURE_GROUPCHATS, true); info.getFeatures().putByte(ApiConstants.FEATURE_STATUS, (byte) 0); info.getFeatures().putByte(ApiConstants.FEATURE_XSTATUS, (byte) 0); service.getCoreService().accountStateChanged(info); service.getCoreService().personalInfo(VkEntityAdapter.vkBuddy2PersonalInfo(myInfo, service.getServiceId(), accessToken.getUserID(), service.getProtocolUid()), true); service.getCoreService().iconBitmap(service.getProtocolUid(), engine.getIcon(myInfo.getPhotoPath()), myInfo.getPhotoPath()); } catch (RequestFailedException e) { onRequestFailed(e); } } private void fillIconMap(List<VkBuddy> buddies) { if (buddies == null) return; for (VkBuddy vkb : buddies) { iconPaths.put(vkb.getUid(), vkb.getPhotoPath()); } } private void renewToken() { showLoginDialog(); } private boolean isTokenFresh() { if (accessToken.isUnexpirable()) { return true; } else { Calendar exp = Calendar.getInstance(); exp.setTime(accessToken.getExpirationTime()); return Calendar.getInstance().before(exp); } } private void showLoginDialog() { String password = service.getCoreService().requestPreference(VkConstants.KEY_PASSWORD); boolean autoSubmitDialog = Boolean.parseBoolean(service.getCoreService().requestPreference(VkConstants.KEY_AUTO_SUBMIT_AUTH_DIALOG)); Bundle options = new Bundle(); options.putCharSequence(VkConstants.KEY_PROTOCOL_ID, service.getProtocolUid()); options.putCharSequence(VkConstants.KEY_PASSWORD, password); options.putBoolean(VkConstants.KEY_AUTO_SUBMIT_AUTH_DIALOG, autoSubmitDialog); Intent intent = new Intent(); intent.setClass(service.getContext(), LoginActivity.class); intent.putExtras(options); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | intent.getFlags()); service.getContext().startActivity(intent); } private void checkTokenAndLogin() { Executors.defaultThreadFactory().newThread(proceedLoginRunnable).start(); } public ConnectionState getConnectionState() { return connectionState; } public void loginResult(String code) { try { accessToken = VkEngine.getAccessToken(code); checkTokenAndLogin(); } catch (RequestFailedException e) { Logger.log(e); onLogout(e.getLocalizedMessage(), ProtocolException.Cause.CANNOT_AUTHORIZE.ordinal()); } } public void loginResult(String token, long expirationTimeMillis, boolean unexpirableToken, long internalUserId) { accessToken = new AccessToken(token, internalUserId, expirationTimeMillis, unexpirableToken); checkTokenAndLogin(); } public void logout() { if (engine != null) { engine.disconnect(null); } onLogout(null); } public void requestAvailableGroupchats() { if (engine == null) { onLogout(null); return; } Executors.defaultThreadFactory().newThread(new Runnable() { @Override public void run() { service.getCoreService().searchResult(VkEntityAdapter.vkChats2PersonalInfoList(new ArrayList<VkChat>(chats), service.getServiceId())); } }).start(); } public void setStatus(final OnlineInfo info) { Executors.defaultThreadFactory().newThread(new Runnable() { @Override public void run() { String statusString; if (!TextUtils.isEmpty(info.getXstatusName()) && !TextUtils.isEmpty(info.getXstatusDescription())) { statusString = info.getXstatusName() + ": " + info.getXstatusDescription(); } else { statusString = TextUtils.isEmpty(info.getXstatusDescription()) ? info.getXstatusName() : info.getXstatusDescription(); } setStatusInternal(statusString); } }).start(); } private void setStatusInternal(String statusString) { if (engine == null) { onLogout(null); return; } if (statusString == null) { statusString = ""; } try { engine.setStatus(statusString); } catch (RequestFailedException e) { onRequestFailed(e); } } private void onLogout(String reason) { this.onLogout(reason, -1); } private void onLogout(String reason, int errorCode) { connectionState = ConnectionState.DISCONNECTED; service.getCoreService().connectionStateChanged(ConnectionState.DISCONNECTED, errorCode); if (reason != null) { service.getCoreService().notification(reason); } } private void getAvailableGroupchats() { if (engine == null) { onLogout(null); return; } Executors.defaultThreadFactory().newThread(new Runnable() { @Override public void run() { try { List<VkChat> vkchats = engine.getGroupChats(); chats.clear(); chats.addAll(vkchats); List<OnlineInfo> onlineInfos = VkEntityAdapter.vkChats2OnlineInfoList(chats, connectedChats, service.getProtocolUid(), service.getServiceId()); service.getCoreService().buddyStateChanged(onlineInfos); } catch (RequestFailedException e) { onRequestFailed(e); } } }).start(); } public void requestIcon(final String uid) { if (connectionState == ConnectionState.CONNECTED) { iconDownloader.addUrl(uid); } } private void requestIconInternal(String uid) { if (engine == null) { onLogout(null); return; } try { long id; if (uid.equals(service.getProtocolUid())) { id = accessToken.getUserID(); } else { id = Long.parseLong(uid); } String path = iconPaths.get(id); if (TextUtils.isEmpty(path)) { Logger.log("No icon available for " + uid, LoggerLevel.VERBOSE); return; } else { Logger.log("Icon request for " + uid + " / " + path, LoggerLevel.VERBOSE); } if (!path.startsWith("https://") && !path.startsWith("http://")) { path = "https://" + path; } byte[] icon = engine.getIcon(path); if (icon != null) { service.getCoreService().iconBitmap(uid, icon, path); } } catch (RequestFailedException e) { onRequestFailed(e); } } private boolean isChatUid(long uid) { for (VkChat chat : chats) { if (chat.getId() == uid) { return true; } } return false; } private boolean isChatJoined(long uid) { return isChatUid(uid) && connectedChats.contains(uid); } public long sendMessage(Message message) { if (engine == null) { onLogout(null); return 0; } long id = Long.parseLong(message.getContactUid()); boolean isChat = isChatUid(id); try { if (isChat) { if (!isChatJoined(id)) { joinChatInternal(message.getContactUid(), false); } } if (message instanceof TextMessage) { return engine.sendMessage(VkEntityAdapter.textMessage2VkMessage((TextMessage) message, isChat)); } } catch (NumberFormatException e) { Logger.log(e); } catch (RequestFailedException e) { onRequestFailed(e); } return 0; } private void onRequestFailed(RequestFailedException e) { if (e != null) { Logger.log(e); onLogout(e.getLocalizedMessage()); } } private final class IconDownloader implements Runnable { private final Set<String> requests = Collections.synchronizedSet(new HashSet<String>()); private volatile boolean isRunning = false; void addUrl(String url) { requests.add(url); if (!isRunning) { Executors.defaultThreadFactory().newThread(this).start(); } } @Override public void run() { isRunning = true; synchronized (requests) { while (requests.size() > 0) { for (Iterator<String> i = requests.iterator(); i.hasNext();) { String url = i.next(); requestIconInternal(url); i.remove(); } } } isRunning = false; } } }